package utilities.control.keyboard;

import java.awt.Container;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;

import utilities.control.enums.KeyEventType;

/** <code>Keyboard</code><br>
 * <br>
 * The <code>Keyboard</code> class is an abstract class that implements the <code>KeyListener</code> 
 * interface. It keeps track of <code>KeyEvents</code> that occur and stores them in descending order of 
 * occurrence.<br>
 * <br>
 * 
 * @author CoconutTank
 *
 */
public abstract class Keyboard implements KeyListener 
{		
	/*******************************************************************
	 * 
	 * CLASS VARIABLES
	 * 
	 *******************************************************************/
	
	/** <code><b>maxEvents</b></code><br>
	 * <br>
	 * The upper limit of how many <code>KeyEvents</code> the <code>Keyboard</code> object will keep track of.<br>
	 * An upper limit is specified for <code>KeyEvents</code> of each type.<br>
	 * <br>
	 * 
	 */
	private int[] maxEvents = null;
	
	/** <code><b>allEvents</b></code><br>
	 * <br>
	 * Stores all recorded <code>KeyEvents</code>.<br>
	 * Each element of the array is a <code>LinkedList</code> for each <code>KeyEvent</code> type.<br>
	 * <br>
	 * 
	 */
	private LinkedList<KeyEvent>[] allEvents = null;
	
	/** <code><b>parent</b></code><br>
	 * <br>
	 * The <code>Container</code> object which has registered this <code>Keyboard</code>. It is
	 * highly recommended that the methods provided by <code>Keyboard</code> are used for 
	 * <code>Container</code> interaction.<br>
	 * <br>
	 * 
	 */
	private Container parent;
	
	/*******************************************************************
	 * 
	 * CONSTRUCTORS
	 * 
	 *******************************************************************/
	
	/** <code>Keyboard</code><br>
	 * <br>
	 * The constructor for <code>Keyboard</code> requires integer arguments to specify how many previous
	 * events it should record at any time. There are no constraints to the arguments that can be specified 
	 * for maximum record count, but giving an argument of 0 or less will make the <code>Keyboard</code> 
	 * object ignore recording those particular types of <code>KeyEvents</code>.<br>
	 * <br>
	 * 
	 * @param maxKeyTypedEventsToRecord
	 * @param maxKeyPressedEventsToRecord
	 * @param maxKeyReleasedEventsToRecord
	 * @param keyboardUser
	 */
	@SuppressWarnings("unchecked")
	public Keyboard(int maxKeyTypedEventsToRecord,
					int maxKeyPressedEventsToRecord,
					int maxKeyReleasedEventsToRecord, 
					Container keyboardUser)
	{
		// Set up max event count values for each event type
		maxEvents = new int[KeyEventType.values().length];
		maxEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()] = maxKeyTypedEventsToRecord;
		maxEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()] = maxKeyPressedEventsToRecord;
		maxEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()] = maxKeyReleasedEventsToRecord;

		// MMMM SUPER HACK
		// THANKS FOR NOT ALLOWING ARRAYS OF GENERICS JAVA .ordinal() 
		// I AM GLAD TO BE USING THIS SOLUTION
		//
		allEvents = (LinkedList<KeyEvent>[]) new LinkedList[KeyEventType.values().length];
		
		allEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()] = ((maxEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()] > 0)
									  ? new LinkedList<KeyEvent>()
									  : null);
		allEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()] = ((maxEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()] > 0)
										? new LinkedList<KeyEvent>()
										: null);
		allEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()] = ((maxEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()] > 0)
									  ? new LinkedList<KeyEvent>()
									  : null);
		
		// Register the Component that will be using this Keyboard, if able
		if((parent = keyboardUser) != null) parent.addKeyListener(this);
	}
	
	/*******************************************************************
	 * 
	 * INHERITED METHODS
	 * 
	 *******************************************************************/
	
	/** <code>keyTyped</code><br>
	 * <br>
	 * Calls the <code>onKeyTyped</code> method with the key typed event that was given 
	 * and records the event. If the number of events to record for key typed is exceeded, 
	 * old events are removed.<br>
	 * <br>
	 * 
	 */
	public final void keyTyped(KeyEvent e)
	{
		if(maxEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()] > 0) allEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()].add(e);
		
		onKeyEvent(KeyEventType.KEY_TYPED_EVENT, e);
		
		while(maxEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()] > 0 && allEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()].size() > maxEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()])
			allEvents[KeyEventType.KEY_TYPED_EVENT.ordinal()].removeFirst();
	}
	
	/** <code>keyPressed</code><br>
	 * <br>
	 * Calls the <code>onKeyPressed</code> method with the key pressed event that was 
	 * given and records the event. If the number of events to record for key pressed is 
	 * exceeded, old events are removed.<br>
	 * <br>
	 * 
	 */
	public final void keyPressed(KeyEvent e) 
	{		
		if(maxEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()] > 0) allEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()].add(e);
		
		onKeyEvent(KeyEventType.KEY_PRESSED_EVENT, e);
		
		while(maxEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()] > 0 && allEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()].size() > maxEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()])
			allEvents[KeyEventType.KEY_PRESSED_EVENT.ordinal()].removeFirst();
	}
	
	/** <code>keyReleased</code><br>
	 * <br>
	 * Calls the <code>onKeyReleased</code> method with the key released event that was 
	 * given and records the event. If the number of events to record for key released
	 * is exceeded, old events are removed.<br>
	 * <br>
	 * 
	 */
	public final void keyReleased(KeyEvent e) 
	{		
		if(maxEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()] > 0) allEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()].add(e);
		
		onKeyEvent(KeyEventType.KEY_RELEASED_EVENT, e);

		while(maxEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()] > 0 && allEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()].size() > maxEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()])
			allEvents[KeyEventType.KEY_RELEASED_EVENT.ordinal()].removeFirst();
	}
	
	/*******************************************************************
	 * 
	 * ABSTRACT METHODS
	 * 
	 *******************************************************************/

	/** <code>onKeyEvent</code><br>
	 * <br>
	 * An exposed stub to allow child classes to respond to <code>KeyEvents</code>.<br>
	 * <br>
	 * @param e
	 */
	public abstract void onKeyEvent(KeyEventType t, KeyEvent e);
	
	/*******************************************************************
	 * 
	 * UTILITIES
	 * 
	 *******************************************************************/
	
	/** <code>numberOfKeyEvents</code><br>
	 * <br>
	 * Returns the number of <code>KeyEvents</code> that are currently on record for the given 
	 * <code>KeyEvent</code> type.<br>
	 * <br>
	 * 
	 * @return
	 */
	public final int numberOfKeyEvents(KeyEventType t)
	{
		// Doing some quick checks to see if any processing is really required.
		//
		if (t != null && allEvents[t.ordinal()] == null) return 0;
				
		return allEvents[t.ordinal()].size();
	}
	
	/** <code>isRecordingEventType</code><br>
	 * <br>
	 * Determines if the given <code>KeyEvent</code> type is being recorded by this <code>Keyboard</code> 
	 * object.<br>
	 * <br>
	 * 
	 * @param keyEventType
	 * @return
	 */
	public final boolean isRecordingEventType(KeyEventType t)
	{
		return (t != null && maxEvents[t.ordinal()] > 0);
	}
	
	/*******************************************************************
	 * 
	 * SETTERS / GETTERS / ACCESSORS
	 * 
	 *******************************************************************/
	
	/** <code>getParent</code><br>
	 * <br>
	 * Returns the <code>Container</code> that is currently using this <code>Keyboard</code>. This 
	 * method will return null if no <code>Container</code> is registered.<br>
	 * <br>
	 * 
	 */
	public final Container getParent() { return parent; }
	
	/** <code>registerParent</code><br>
	 * <br>
	 * Registers the given <code>Container</code> as the parent using this <code>Keyboard</code>, 
	 * and adds this <code>Keyboard</code> as a key listener to the <code>Container</code>.<br>
	 * Each <code>Keyboard</code> object may only have one parent at a time; if this 
	 * <code>Keyboard</code> has a non-null parent, no changes are made.<br>
	 * <br>
	 * 
	 * @param keyboardUser
	 */
	public final void registerParent(Container keyboardUser)
	{
		if(parent == null && (parent = keyboardUser) != null)
			parent.addKeyListener(this);
		else
			System.out.println("Could not register given Container to this Keyboard.");
	}
	
	/** <code>unregisterParent</code><br>
	 * <br>
	 * Unregisters and returns the <code>Container</code> using this <code>Keyboard</code>, and also
	 * removes this <code>Keyboard</code> from the <code>Container's</code> set of key 
	 * listeners.<br>
	 * <br>
	 * Returns null if no parent <code>Container</code> is registered.<br>
	 * <br>
	 * 
	 * @return
	 */
	public final Container unregisterParent()
	{
		Container old = parent;
		
		if(parent != null)
		{
			parent.removeKeyListener(this);
			parent = null;
		}
		
		return old;
	}
	
	/** <code>getLastKeyEvent</code><br>
	 * <br>
	 * Returns the last <code>KeyEvent</code> for the specified <code>KeyEvent</code> type.<br>
	 * <br>
	 * This method returns null for the following:<br>
	 * <li>The given <code>KeyEvent</code> type is invalid: refer to <code>keyEventTypeIsValid</code>.</li><br>
	 * <li>There are no <code>KeyEvents</code> recorded for the given <code>KeyEvent</code> type.</li><br>
	 * <br>
	 * 
	 * @return
	 */
	public final KeyEvent getLastKeyEvent(KeyEventType t)
	{		
		return (t != null 
				&& allEvents[t.ordinal()] != null 
				&& allEvents[t.ordinal()].size() > 0 
				? allEvents[t.ordinal()].getLast() 
				: null);
	}
	
	/** <code>getLastTyped</code><br>
	 * <br>
	 * Returns the nth last <code>KeyEvent</code> to occur for the key typed event. If a match is found, it
	 * returns the matching <code>KeyEvent</code>.<br>
	 * <br>
	 * This method returns null for any of the following:<br>
	 * <li>The given key event type is invalid: refer to <code>keyEventTypeIsValid</code>.</li><br>
	 * <li>The argument for <b>nthLastEvent</b> is 0 or less.</li><br>
	 * <li>There are no <code>KeyEvents</code> recorded for the given <code>KeyEvent</code> type ID.</li><br>
	 * <li>There is no nth last <code>KeyEvent</code> for the given arguments.</li><br>
	 * <br>
	 * 
	 * @param nthLastEvent
	 * @return
	 */
	public final KeyEvent getNthLastKeyEvent(KeyEventType t, int nthLastEvent)
	{
		return (t != null
				&& nthLastEvent > 0 
				&& allEvents[t.ordinal()] != null 
				&& allEvents[t.ordinal()].size() >= nthLastEvent 
				? allEvents[t.ordinal()].get(allEvents[t.ordinal()].size() - nthLastEvent)
				: null);
	}
	
	/** <code>getKeyEventsInTimeSpan</code><br>
	 * <br>
	 * Returns a list of key events inclusive to the specified time range (from <b>timeStart</b> to 
	 * <b>timeEnd</b>) and of the key event type given.<br>
	 * <br>
	 * This method returns null for any of the following:<br>
	 * <li>The given key event type is invalid: refer to <code>keyEventTypeIsValid</code>.</li><br>
	 * <li>There are no key events recorded for the given <code>KeyEvent</code> type ID.</li><br>
	 * <li>The specified time range is illogical; either <b>timeEnd</b> is less than <b>timeStart</b>, or 
	 * <b>timeEnd</b> is less than 1.</li><br>
	 * <li>No matching key events are found for the given time range and <code>KeyEvent</code> type.</li><br>
	 * <br>
	 * 
	 * @param timeStart
	 * @param timeEnd
	 * @return
	 */
	public final ArrayList<KeyEvent> getKeyEventsInTimeSpan(KeyEventType t, long timeStart, long timeEnd)
	{
		// Doing some quick checks to see if any processing is really required.
		//
		if (t == null) return null;
		if (allEvents[t.ordinal()] == null || allEvents[t.ordinal()].size() < 1) return null;
		if (timeEnd < timeStart || timeEnd < 1) return null;
		
		// Set up variables
		//
		ArrayList<KeyEvent> results = new ArrayList<KeyEvent>();		
		KeyEvent iterEvent = null;
		
		// Iterator should go from the end of the list (most recent) to the start of the list (least recent).
		//
		Iterator<KeyEvent> eventsList = allEvents[t.ordinal()].descendingIterator();

		// Loop over all key events and look for any key events that fall within the specified 
		// time range.
		//
		while(eventsList != null && eventsList.hasNext())
		{
			// Make the iterator go a little faster.
			// - Keep going if nulls are found.
			// - If a non-null is found, keep going if it is still outside of the specified time range.
			//
			do
			{
				while((iterEvent = eventsList.next()) == null && eventsList.hasNext());
			}
			while(iterEvent.getWhen() > timeEnd && eventsList.hasNext());
			
			// Verify that the key event's occurrence falls within the specified time range.
			//
			if(iterEvent != null && iterEvent.getWhen() >= timeStart && iterEvent.getWhen() <= timeEnd)
				results.add(iterEvent);
			
			// Loop breaking condition:
			// - If any KeyEvent is reached such that its recorded time is older time than the lower time
			// range limit, this loop will break.
			// - It is assumed that all pairs recorded are recorded in sequential order of occurrence.
			//
			if(iterEvent != null && iterEvent.getWhen() < timeStart)
				eventsList = null;
		}
		
		// Return non-null only if there are any results to give.
		//
		return (results.size() > 0 ? results : null);
	}
}